package org.robolectric.annotation.processing.generator;

import com.google.common.base.Joiner;
import org.robolectric.annotation.processing.RobolectricModel;
import org.robolectric.annotation.processing.RobolectricProcessor;

import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;

/**
 * Generator that creates the "ShadowProvider" implementation for a shadow package.
 */
public class ShadowProviderGenerator extends Generator {
  private final Filer filer;
  private final Messager messager;
  private final RobolectricModel model;

  public ShadowProviderGenerator(RobolectricModel model, ProcessingEnvironment environment) {
    this.messager = environment.getMessager();
    this.filer = environment.getFiler();
    this.model = model;
  }

  @Override
  public void generate(String shadowPackage) {
    final String shadowClassName = shadowPackage + '.' + GEN_CLASS;
    messager.printMessage(Diagnostic.Kind.NOTE, "Generating output file: " + shadowClassName);

    // TODO: Because this was fairly simple to begin with I haven't
    // included a templating engine like Velocity but simply used
    // raw print() statements, in an effort to reduce the number of
    // dependencies that RAP has. However, if it gets too complicated
    // then using Velocity might be a good idea.
    PrintWriter writer = null;
    try {
      JavaFileObject jfo = filer.createSourceFile(shadowClassName);
      writer = new PrintWriter(jfo.openWriter());
      writer.print("package " + shadowPackage + ";\n");
      for (String name : model.getImports()) {
        writer.println("import " + name + ';');
      }
      writer.println();
      writer.println("/**");
      writer.println(" * Shadow mapper. Automatically generated by the Robolectric Annotation Processor.");
      writer.println(" */");
      writer.println("@Generated(\"" + RobolectricProcessor.class.getCanonicalName() + "\")");
      writer.println("@SuppressWarnings({\"unchecked\",\"deprecation\"})");
      writer.println("public class " + GEN_CLASS + " implements ShadowProvider {");
      writer.println();

      for (Map.Entry<TypeElement, TypeElement> entry : model.getShadowOfMap().entrySet()) {
        final TypeElement actualType = entry.getValue();
        if (!actualType.getModifiers().contains(Modifier.PUBLIC)) {
          continue;
        }
        int paramCount = 0;
        StringBuilder paramDef = new StringBuilder("<");
        StringBuilder paramUse = new StringBuilder("<");
        for (TypeParameterElement typeParam : entry.getValue().getTypeParameters()) {
          if (paramCount > 0) {
            paramDef.append(',');
            paramUse.append(',');
          }
          boolean first = true;
          paramDef.append(typeParam);
          paramUse.append(typeParam);
          for (TypeMirror bound : model.getExplicitBounds(typeParam)) {
            if (first) {
              paramDef.append(" extends ");
              first = false;
            } else {
              paramDef.append(" & ");
            }
            paramDef.append(model.getReferentFor(bound));
          }
          paramCount++;
        }
        String paramDefStr = "";
        String paramUseStr = "";
        if (paramCount > 0) {
          paramDefStr = paramDef.append("> ").toString();
          paramUseStr = paramUse.append('>').toString();
        }
        final String actual = model.getReferentFor(actualType) + paramUseStr;
        final String shadow = model.getReferentFor(entry.getKey()) + paramUseStr;
        writer.println("  public static " + paramDefStr + shadow + " shadowOf(" + actual + " actual) {");
        writer.println("    return (" + shadow + ") ShadowExtractor.extract(actual);");
        writer.println("  }");
        writer.println();
      }

      writer.println("  public void reset() {");
      for (Map.Entry<TypeElement, ExecutableElement> entry : model.getResetters()) {
        writer.println("    " + model.getReferentFor(entry.getKey()) + "." + entry.getValue().getSimpleName() + "();");
      }
      writer.println("  }");
      writer.println();

      writer.println("  public String[] getProvidedPackageNames() {");
      writer.println("    return new String[] {" + Joiner.on(",").join(model.getShadowedPackages()) + "};");
      writer.println("  }");

      writer.println('}');
    } catch (IOException e) {
      messager.printMessage(Diagnostic.Kind.ERROR, "Failed to write shadow class file: " + e);
      throw new RuntimeException(e);

    } finally {
      if (writer != null) {
        writer.close();
      }
    }
  }
}
